## Функции высшего порядка и функциональные паттерны

### lambda
### map
### reduce
### filter
### хранение функций в словарях

## Магия


```python
def f():
    f.a = 1


f()
f.b = 2
f.__setattr__('c', 3)

print(f.__dict__)

```


## 1. lambda

Это занятие будет посвящено, так называемым, анонимным или, их еще называют, lambda функциям. 
Что это за функции и почему их называют анонимными? Сейчас вы все узнаете!
Но для начала вспомним как работает функция map

```python

s = 'Hello Python from Russia'

res = s.split()

print(res)
#Результат
['Hello', 'Python', 'from', 'Russia']
А как получить длины слов
res = list(map(len, s.split()))

print(res)
```

А сейчас?
```python
def my_len2(x):return len(x)

res = list(map(my_len2, s.split()))

# А можно сделать вот так 
my_len1 = lambda string: len(string)
res = list(map(my_len1, s.split()))

# Или даже так
res = list(map(lambda x: len(x), s.split()))

# Что происходит, посмотрим на второй пример:
s = [1, 2, 3, 4, 5]
res = list(map(lambda x: x ** 2, s))

print(res)
```

Результат

> [1, 4, 9, 16, 25]

Похожий результат дает генератор! 

> res2 = [x ** 2 for x in s]

А давайте вынесем нашу ламбаду

> lmd = lambda x: x ** 2

> res1 = list(map(lmd, s))

понятно как работает? Закрепим!
> s = ['Hello', 'Python', 'from', 'Russia']

```python
my_len1 = lambda string: len(string)

def my_len2(x):return len(x)

res1 = list(map(my_len1, s))
res2 = list(map(my_len2, s))

print(res1)
print(res2)
```

> lambda a, b: a + b

В чем особенность такого определения функции? Зачем ее придумали и почему бы не пользоваться обычными функциями? У нее есть одно принципиальное отличие от ранее рассматриваемых нами функций – она может быть записана как элемент любой конструкции языка Python. Например, прямо как элемент списка:

>a = [4, 5, lambda: print("lambda"), 7, 8]

Однако, у таких функций есть одно существенное ограничение – в них можно прописать только одну конструкцию языка Python, то есть, выполнить только одну какую-либо команду. 

Также нельзя объявлять анонимные функции в несколько строк:
```python
lambda a: 
    print(a)
```

Аналогичным образом можно преобразовывать коллекции более сложных объектов:

```python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age 
         
people = [ Person("Tom", 38), Person("Kate", 31), Person("Bob", 42), 
        Person("Alice", 34),  Person("Sam", 25) ]
 
# получаем из Person строку с именем
view = map(lambda p: p.name, people)
  
for person in view:
    print(person)

```
Здесь проекция применяется к списку объектов `Person`. 

Функция преобразование получает каждый объект `Person` и возвращает значение его атрибута `name`. 

То есть полученный в результате список будет содержать набор строк (атрибуты name всех объектов `Person`).

Консольный вывод:

```
Tom
Kate
Bob
Alice
Sam
```



## 2. lambda + key

как можно управлять алгоритмом сортировки с помощью специального параметра key, который имеется у метода sort() и функции sorted().
По умолчанию сортировка коллекции выполняется по значениям ее элементов. Например:

```python
a = [4, 3, -10, 1, 7, 12]
b = sorted(a)
print(b)
```

Но мы можем вместо этих значений указать другие, которые будут использованы для сортировки элементов.

Например, вычислить показатель четности значений. Тогда последовательность будет выстроена по возрастанию этих ключей. В результате увидим сначала четные значения, а потом – нечетные.

Чтобы выполнить такую манипуляцию в функции sorted() прописывается аргумент key и ему присваивается ссылка на функцию, которая будет формировать альтернативное значение элемента, то есть, ключ:

```python
b = sorted(a, key=is_odd)
А саму функцию можно определить, следующим образом:
def is_odd(x):
    return x % 2
```

Здесь аргумент x – это текущее значение элемента коллекции, а то, что она возвращает, становится значением соответствующего ключа. 

То есть, для четных значений будем иметь 0, а для нечетных – 1. После запуска программы видим искомый результат сортировки.

Конечно, для простых функций, обычно, в аргументе key записывают лямбда-функцию. В нашем примере она будет выглядеть, следующим образом:

```python
b = sorted(a, key=lambda x: x % 2)
```
Как видите, все достаточно просто.
Тот же самый аргумент key можно указывать и в методе `sort()` для списка:

```python
a.sort(key=lambda x: x % 2)
```
Он здесь работает абсолютно также, как и в функции `sorted()`.

Предположим, что у нас имеется список городов:
```python
lst = ["Москва", "Тверь", "Смоленск", "Псков", "Рязань"]
```
И требуется их выстроить по длине. Для этого воспользуемся функцией `sorted()` и в аргументе key укажем стандартную функцию `len`:

```python
print( sorted(lst, key=len) )
# получим следующий результат:
['Тверь', 'Псков', 'Москва', 'Рязань', 'Смоленск']
# Или можно сделать сортировку по последнему символу слова:
print( sorted(lst, key=lambda x: x[-1]) )
['Москва', 'Псков', 'Смоленск', 'Тверь', 'Рязань']
# Или только по первому:
print( sorted(lst, key=lambda x: x[0]) )
['Москва', 'Псков', 'Рязань', 'Смоленск', 'Тверь']
И так далее.
```

Аргумент `key` часто используют для сортировки сложных структур данных. 

Допустим, у нас имеется вот такой кортеж, содержащий вложенные кортежи с информацией по книгам:
```python
books = (
    ("Евгений Онегин", "Пушкин А.С.", 200),
    ("Муму", "Тургенев И.С.", 250),
    ("Мастер и Маргарита", "Булгаков М.А.", 500),
    ("Мертвые души", "Гоголь Н.В.", 190)
)
```
И нам нужно его отсортировать по цене (последнее значение). Очевидно, это можно сделать так:
> print( sorted(books, key=lambda x: x[2]) )

На выходе получим отсортированный список:

```python
[('Мертвые души', 'Гоголь Н.В.', 190), ('Евгений Онегин', 'Пушкин А.С.', 200), ('Муму', 'Тургенев И.С.', 250), ('Мастер и Маргарита', 'Булгаков М.А.', 500)]
```


Вот так используется аргумент `key` для управления сортировкой элементов произвольных коллекций данных. 

И теперь вы знаете, как его применять в своих программах.
Рассмотрим еще один пример

На складе лежат мешки различного веса и стоимости. Вес за мешок и стоимость записаны на как натуральные числа с списке. Отсортировать мешки по стоимости за кг, при равной стоимости первыми идут мешки с наименьшим весом

```
S = [
    [47, 470],
    [50, 600],
    [60, 480],
    [45, 540],
    [30, 300]
]
```

```
S0 = [(x[1]/x[0], x[0], x[1])  for x in S]
print(S0)
S0.sort(key=lambda x: (x[0], x[2]))
print(S0)

```


### Функция filter

```python
S.sort( key = lambda x: (x[1]/x[0], -x[1]) )
numbers = [1, 2, 4, 5, 7, 8, 10, 11]

# функция, которая проверяет числа
def filter_odd_num(in_num):
    if(in_num % 2) == 0:
        return True
    else:
        return False
```

# Применение filter() для удаления нечетных чисел

```python
out_filter = filter(filter_odd_num, numbers)

print("Тип объекта out_filter: ", type(out_filter))
print("Отфильтрованный список: ", list(out_filter))
```
еще пример
```python
lst = ("Москва", "Рязань1", "Смоленск", "Тверь2", "Томск")
b = filter(str.isalpha, lst)

def f(s):
    return s[-1] =="а"

c = filter(f, lst)
print(*b)
print(*c)
```

### Задача 2. Отфильтровать стоп-слова (союзы и предлоги) из строки.

Аналогичным образом можно отфильтровать списки более сложных объектов:

```python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age 
         
people = [ Person("Tom", 38), Person("Kate", 31), Person("Bob", 42), 
        Person("Alice", 34),  Person("Sam", 25) ]
 
# фильтрация элементов, у которых age > 33
view = filter(lambda p: p.age > 33, people)
  
for person in view:
    print(f"Name: {person.name} Age: {person.age}")
```

В данном случае фильтруем список объектов Person, поэтому в функцию-условие/лямбда-выражение в качестве параметра передается каждый объект `Person` из списка. 

Каждый объект `Person` хранит имя `(Name)` и возраст `(Age)`, и здесь выбираем всех `Person`, у которых возраст больше `33`.

## reduce

Последняя функция из нашей тройки — `reduce()` (говорят "свертка"), который используется для агрегации данных.

Под агрегацией понимается операция, вычисляющая значение, зависящее от всего набора данных. С помощью функции reduce можно последовательно применить операции к элементам списка, чтобы получить единственное значение.

```python
def add(x, y):
    return x + y

def multiply(x, y):
    return x * y

numbers = [1, 2, 3, 4, 5]
result = reduce(add, numbers, 0)
print(result)
# 15

result = reduce(multiply, numbers, 1)
print(result)
```

Внутри reduce можно использовать и лямбда функции


Аналогичным образом можно получить и выполнить другие функции.


Например, передача лямбда-выражения в качестве параметра:

```python
def do_operation(a, b, operation):
    return operation(a, b)



x1 = do_operation(5, 4, lambda a, b: a + b)
x2 = do_operation(5, 4, lambda a, b: a * b)

print(x1, x2)
```
Бессмысленный пример, но это основа для понимания декораторов и замыканий функций в дальнейшем!

В данном случае нам нет необходимости определять функции, чтобы передать их в качестве параметра, как в прошлом примере.
То же самое касается и возвращение лямбда-выражений из функций:
```python
def select_operation(choice):
    if choice == 1:
        return lambda a, b: a + b
    elif choice == 2:
        return lambda a, b: a - b
    else:
        return lambda a, b: a * b


operation = select_operation(1)  # operation = sum
print(help(operation)) 
```

Результат:
> <lambda> lambda a, b

Теперь в переменной operation лежит lambda функция
```python
print(operation(10, 6))  # 16

operation = select_operation(2)  # operation = subtract
print(operation(10, 6))  # 4

operation = select_operation(3)  # operation = multiply
print(operation(10, 6))  # 60
```
